[Design Pattern] Visitor

[디자인 패턴][행위 패턴] 방문자

Posted by ChaelinJ on May 23, 2021

Visitor

실제 로직을 가지고 있는 객체(Visitor)가 로직을 적용할 객체를 방문하면서 실행하는 패턴

  • 방문 공간 객체는 방문자 객체에 행동을 위임
  • 로직과 구조를 분리하는 패턴, 구조를 수정하지 않고도 새로운 동작을 기존 객체에 추가 가능

일반적인 OOP의 경우, 객체 내에 로직을 메소드로 가지고 있고 대상이 되는 객체가 있을 경우 파라미터로 입력을 받아야 합니다.

방문자 패턴의 경우엔 반대로 대상이 되는 객체(방문 공간)가 행동을 일으키는 객체(방문자)를 입력받으며 방문자 객체에 행동을 위임합니다.

다이어그램

  • Element: 방문 공간 클래스의 인터페이스
    • 알고리즘의 구조를 가지고 있습니다.
  • Visitor: 방문자 클래스의 인터페이스
    • 로직을 가지고 있습니다.

장점

  • 로직 수정시, Element의 변경 없이 Visitor 객체만 수정하면 됩니다.
  • 수행할 구체적 작업을 분리하여 구현할 수 있어 유지보수와 재사용이 용이해집니다.

단점

  • Visitor 객체와 Element 객체는 서로 Accept와 Visit에 의존해 결합도가 높은 편입니다.
  • Visitor 객체가 속성값을 직접 제어하므로 정보은닉이 필요한 부분까지 노출되어 캡슐화가 약해지고 신뢰성이 떨어질 수 있습니다.
  • Element가 추가될 때마다 Visitor도 이에 대한 로직을 추가해야 합니다.

에제와 이해

할인마트에서 물건을 판매하고 있습니다. 상품에는 각각의 라벨이 있는데 빨간 라벨 상품과 초록 라벨 상품이 있습니다.

  • 빨간 라벨 상품
    • 할인: 50%
    • 적립: 1%
  • 초록 라벨 상품
    • 할인: 10%
    • 적립: 5%

위 같은 차이가 있을 때 고객이 담아온 물건 별로 할인과 적립 혜택이 적용되도록 구현해보았습니다.

1단계 - 상품 인터페이스와 구현 클래스

각각 상품 클래스 내에 할인과 적립 내용을 저장합니다.

Apply Discount 50%
Add Point 1%
Apply Discount 10%
Add Point 5%

이렇게 할 경우, 순회를 통해 상품 리스트를 순회하며 할인을 적용하고 적립할 수 있습니다.

다만 상품을 사면 상품권을 추가 증정한다는 등의 행동을 추가해야 한다면 모든 Merchandise 구현 클래스들에 대해 수정해주어야 합니다. (단일 책임 원칙 위반)

이를 수정하기 위해 구조와 로직을 구분하여 상품과 혜택(적립, 할인)을 구분했습니다.

2단계 - 상품과 혜택 구분

혜택을 따로 인터페이스와 구현 클래스를 만들어 적용하였습니다.

BenefitImpl의 메소드를 통해 여러 해택을 적용할 수 있으며 해택을 추가하고자 할 땐 혜택 관련 인터페이스와 클래스를 수정하면 됩니다.

상품 구현 클래스 별로 혜택의 차이가 있기 때문에 구현 클래스를 파라미터로 입력 받도록 오버로딩 했습니다.

하지만 위처럼 상품 구현 객체를 인자로 넣으면 오류가 납니다.

md1과 md2는 Merchandise 객체이기 때문에 구현 클래스를 파라미터로 하는 메소드를 사용할 수 없기 때문입니다.

물론 Merchandise로 생성하지 않고 구현 클래스로 생성하면 정상적으로 작동합니다. 하지만 이렇게 하면 Merchandise interface를 이용하므로써 순회를 이용할 수 있다는 장점이 없어집니다.

3단계 - Visitor 패턴 적용

방문 공간(상품)에 어떤 방문자(Benefit)가 방문하는지에 따라 행동이 달라집니다.

만약 DiscountBenefit 객체라면 할인 혜택을 위한 동작을, PointBenefit 객체라면 적립 혜택을 위한 동작을 수행합니다.

Merchandise - Element
Benefit - Visitor
Main
Apply Discount 50%
Add Point 1%
Apply Discount 10%
Add Point 5%

어떤 Visitor 객체인지에 따라 다른 수행으로 출력되는 것을 확인할 수 있습니다.

만약 여기서 새로운 혜택을 추가한다면 Benefit 인터페이스 구현 클래스를 하나 생성하면 간단하게 새로운 로직을 추가할 수 있습니다.

하지만 상품 라벨을 파란색을 도입한다면 복잡해집니다. 우선 상품 클래스를 생성해야하고 Benfit 인터페이스를 수정해야 합니다. 그와 함께 Benefit 인터페이스를 구현하는 모든 클래스도 수정해야 합니다.

  • Visitor 추가/삭제는 쉽다 - 알고리즘 추가/수정/삭제는 용이하다.
  • Element 추가/삭제는 어렵다 - 자료구조의 추가/수정/삭제는 어렵다.

사용하면 좋은 경우

  • 자료구조에 비해 알고리즘 수정이 잦은 경우
  • 구조와 로직을 구분하고자 할 경우

참조

감사합니다.

Text by Chaelin. Photographs by Chaelin, Unsplash.